package com.violet.utils;

import com.violet.annotation.transition.*;
import com.violet.domain.dto.DTO;
import com.violet.domain.po.PO;
import com.violet.domain.vo.VO;
import com.violet.properties.FileUploadProperties;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

@Component
@RequiredArgsConstructor
public class TransitionUtils<D extends DTO, P extends PO, V extends VO> {

    // 缓存类的字段数组，减少反射开销
    private static final Map<Class<?>, Field[]> fieldCache = new ConcurrentHashMap<>();
    // 缓存构造器
    private static final Map<Class<?>, Constructor<?>> constructorCache = new ConcurrentHashMap<>();

    private final FileUploadProperties properties;

    // DTO ===> PO
    public P transition(D dto, Class<P> pClass) {
        return this.baseTransition(dto, pClass);
    }

    // PO ===> VO
    public V transition(P po, Class<V> vClass) {
        return this.baseTransition(po, vClass);
    }

    public void copy(Object d, Object p) {
        Field[] sources = this.getFields(d.getClass());
        Field[] targets = this.getFields(p.getClass());
        this.copy2(sources, d, targets, p);
    }

    private <R> R baseTransition(Object o, Class<R> rClass) {
        try {
            Constructor<R> constructor = this.getConstructor(rClass);
            R r = constructor.newInstance();
            Field[] pFields = this.getFields(o.getClass());
            Field[] vFields = this.getFields(rClass);
            this.copy1(pFields, o, vFields, r);
            return r;
        }
        catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * 将源对象的数据复制到目标对象中（注重于类型转换：DTO => PO => VO）
     * @param sources 源对象的字段数组
     * @param o1 源对象
     * @param targets 目标对象的字段数组
     * @param o2 目标对象
     */
    private void copy1(Field[] sources, Object o1, Field[] targets, Object o2) {
        for (Field target: targets) {
            try {
                // 当前字段是否跳过
                if (target.getAnnotation(TNoTransition.class) != null) continue;
                if (target.getAnnotation(TNoTransitionWithDefaultId.class) != null) {
                    this.tNoTransitionWithDefaultId(target, o2);
                    continue;
                }
                Field source  = this.getField(sources, target.getName());
                // 看 source 是否存在
                if (source == null) {
                    // source: Object, target: Object
                    if (target.getAnnotation(TDefaultValue.class) != null) this.tDefaultValue(target, o2);
                    // source: Object, target: String
                    if (target.getAnnotation(TDefaultId.class) != null) this.tDefaultId(target, o2);
                }
                else {
                    // 类型一致且不为 null
                    if (source.getType() == target.getType() && source.get(o1) != null) {
                        // 先赋值，后续处理
                        target.set(o2, source.get(o1));
                        // 不涉及到上游
                        if (target.getAnnotation(TSub.class) != null) this.tSub(target, o2);
                        if (target.getAnnotation(THidePhone.class) != null) this.tHidePhone(target, o2);
                        if (target.getAnnotation(TFormPrice.class) != null) this.tFormPrice(target, o2);
                        if (target.getAnnotation(TImagePrefix.class) != null) this.tImagePrefix(target, o2);
                    }
                    // 类型不一致且不为 null
                    else if (source.getType() != target.getType() && source.get(o1) != null) {
                        // 涉及到上游
                        if (target.getAnnotation(TSplit.class) != null) this.tSplit(source, o1, target, o2);
                        if (target.getAnnotation(TBooleanString.class) != null) this.tBooleanString(source, o1, target, o2);
                        if (target.getAnnotation(TDateTimeFormat.class) != null) this.tDateTimeFormat(source, o1, target, o2);
                    }
                    // 是 null
                    else if (source.get(o1) == null) {
                        if (target.getAnnotation(TDefaultValue.class) != null) this.tDefaultValue(target, o2);
                        if (target.getAnnotation(TDefaultId.class) != null) this.tDefaultId(target, o2);
                    }
                }
            }
            catch (IllegalAccessException e) { throw new RuntimeException(e); }
        }
    }

    /**
     * 将源对象的数据复制到目标对象中（注重于同类型数据拷贝）
     * @param sources 源对象的字段数组
     * @param o1 源对象
     * @param targets 目标对象的字段数组
     * @param o2 目标对象
     */
    private void copy2(Field[] sources, Object o1, Field[] targets, Object o2) {
        for (Field target : targets) {
            // 当前字段是否不进行 copy
            if (target.getAnnotation(CNoCopy.class) != null) continue;
            // 判断当前字段是否需要自动更新
            if (target.getAnnotation(CAutoUpdate.class) != null) {
                this.cAutoUpdate(target, o2);
                continue;
            }
            Field src = this.getField(sources, target.getName());
            if (src != null) {
                try {
                    target.set(o2, src.get(o1));
                }
                catch (IllegalAccessException e) { throw new RuntimeException(e); }
            }
        }
    }

    /**
     * 如果缓存中不存在，则创建缓存，否则直接返回缓存
     * @param clazz 类型
     * @return 对应类型的字段数组
     */
    private Field[] getFields(Class<?> clazz) {
        return fieldCache.computeIfAbsent(clazz, k -> {
            Field[] fields = clazz.getDeclaredFields();
            Arrays.stream(fields).forEach(field -> field.setAccessible(true));
            return fields;
        });
    }

    private <R> Constructor<R> getConstructor(Class<R> clazz) {
        return (Constructor<R>) constructorCache.computeIfAbsent(clazz, k -> {
            try {
                Constructor<R> constructor = clazz.getConstructor();
                constructor.setAccessible(true);
                return constructor;
            }
            catch (NoSuchMethodException e) {throw new RuntimeException("必须提供一个 public 的无参构造方法");}
        });
    }

    private Field getField(Field[] fields, String fieldName) {
        for (Field field : fields) {
            if (field.getName().equals(fieldName)) return field;
        }
        return null;
    }

    private void tSub(Field field, Object o) throws IllegalAccessException {
        // 判断字段类型
        if (field.getType() != String.class)
            throw new RuntimeException("@TSub 只能用于 String 类型的字段: " + o.getClass() + "." + field.getName());

        // 进行截取操作
        TSub tSub = field.getAnnotation(TSub.class);
        String value = field.get(o).toString();
        String[] split = value.split(tSub.value());
        int index = tSub.index();
        // 如果是 index 是负数则反则取, 如果是正数则正则取
        if (index < 0) {
            int length = split.length + index;
            value = split[length];
        }
        else value = split[index];
        field.set(o, value);
    }

    private void tHidePhone(Field field, Object o) throws IllegalAccessException {
        // 判断字段类型
        if (field.getType() != String.class)
            throw new RuntimeException("@THidePhone 只能用于 String 类型的字段: " + o.getClass() + "." + field.getName());
        // 进行隐藏操作
        THidePhone tHidePhone = field.getAnnotation(THidePhone.class);
        String value = field.get(o).toString();
        StringBuilder sb = new StringBuilder();
        for (int i =0; i < value.length(); i++) {
            if (i >= 3 && i <= 7) sb.append(tHidePhone.value());
            else sb.append(value.charAt(i));
        }
        field.set(o, sb.toString());
    }

    private void tFormPrice(Field field, Object o) throws IllegalAccessException {
        // 判断字段类型
        if (field.getType() != Double.class)
            throw new RuntimeException("@TFormPrice 只能用于 Double 类型的字段: " + o.getClass() + "." + field.getName());
        // 进行格式化操作
        TFormPrice tFormPrice = field.getAnnotation(TFormPrice.class);
        // 对价格保留指定位小数
        field.set(o, BigDecimal.valueOf((Double) field.get(o)).setScale(tFormPrice.value(), RoundingMode.HALF_UP).doubleValue());
    }

    private void tSplit(Field source, Object o1, Field target, Object o2) {
        if (source.getType() != String.class)
            throw new RuntimeException("@TSplit 对应的字段应该是 String: " + o2.getClass() + "." + target.getName());

        if (target.getType() != String[].class)
            throw new RuntimeException("@TSplit 只能用于 String[] 类型的字段: " + o2.getClass() + "." + target.getName());

        try {
            TSplit tSplit = target.getAnnotation(TSplit.class);
            if (source.get(o1) != null) {
                String value = source.get(o1).toString();
                if (!value.isEmpty()) {
                    String[] split = value.split(tSplit.value());
                    target.set(o2, split);
                } else {
                    target.set(o2, new String[]{});
                }
            }
            else if (tSplit.fillEmpty()) {
                // 如果为 Null 则设置为空数组
                target.set(o2, new String[]{});
            }
        }
        catch (IllegalAccessException e) { throw new RuntimeException(e); }
    }

    private void tBooleanString(Field source, Object o1, Field target, Object o2) {
        if (source.getType() != Boolean.class)
            throw new RuntimeException("@TBooleanString 只能用于 Boolean 类型的字段: " + o2.getClass() + "." + target.getName());

        if (target.getType() != String.class)
            throw new RuntimeException("@TBooleanString 只能用于 String 类型的字段: " + o2.getClass() + "." + target.getName());

        try {
            TBooleanString tBooleanString = target.getAnnotation(TBooleanString.class);
            if (source.get(o1) != null) {
                boolean value = (boolean) source.get(o1);
                if (value) target.set(o2, tBooleanString.trueValue());
                else target.set(o2, tBooleanString.falseValue());
            }
        }
        catch (IllegalAccessException e) { throw new RuntimeException(e); }
    }

    private void tDefaultValue(Field target, Object o2) {
        // 当前字段值是否为 null, 如果是 null 则根据字段类型设置对应的默认值
        // 只能设置基本类型
        try {
            if (target.get(o2) == null) {
                Class<?> type = target.getType();
                if (type == String.class) target.set(o2, "");
                else if (type == Byte.class) target.set(o2, (byte) 0);
                else if (type == Short.class) target.set(o2, (short) 0);
                else if (type == Integer.class) target.set(o2, 0);
                else if (type == Long.class) target.set(o2, 0L);
                else if (type == Float.class) target.set(o2, 0F);
                else if (type == Double.class) target.set(o2, 0D);
                else if (type == Boolean.class) target.set(o2, false);
                else if (type == Character.class) target.set(o2, ' ');
                else if (type == LocalDateTime.class) target.set(o2, LocalDateTime.now());
                else throw new RuntimeException("@TDefaultValue 只能用于基本类型: " + o2.getClass() + "." + target.getName());
            }
        } catch (IllegalAccessException e) { throw new RuntimeException(e); }
    }

    private void tDefaultId(Field target, Object o2) {
        // 判断是否为 String 类型
        if (target.getType() != String.class)
            throw new RuntimeException("@TDefaultId 只能用于 String 类型的字段: " + o2.getClass() + "." + target.getName());

        try {
            TDefaultId tDefaultId = target.getAnnotation(TDefaultId.class);
            if (target.get(o2) == null || target.get(o2).toString().isEmpty())
                target.set(o2, IdUtils.getId(tDefaultId.prefix()));
        } catch (IllegalAccessException e) { throw new RuntimeException(e); }
    }

    private void tImagePrefix(Field target, Object o2) {
        // 只能对字符串类型的字段进行操作
        if (target.getType() != String.class)
            throw new RuntimeException("@TImagePrefix 只能用于 String 类型的字段: " + o2.getClass() + "." + target.getName());

        try {
            String value = target.get(o2).toString();
            target.set(o2, properties.getImage().url() + value);
        }
        catch (IllegalAccessException e) { throw new RuntimeException(e); }
    }

    private void tNoTransitionWithDefaultId(Field target, Object o2) {
        // 判断是否为 String 类型
        if (target.getType() != String.class)
            throw new RuntimeException("@TNoTransitionWithDefaultId 只能用于 String 类型的字段: " + o2.getClass() + "." + target.getName());

        try {
            TNoTransitionWithDefaultId tNoTransitionWithDefaultId = target.getAnnotation(TNoTransitionWithDefaultId.class);
            String prefix = tNoTransitionWithDefaultId.prefix();
            target.set(o2, IdUtils.getId(prefix));
        } catch (IllegalAccessException e) { throw new RuntimeException(e); }
    }

    private void tDateTimeFormat(Field source, Object o1, Field target, Object o2) {
        if (source.getType() != LocalDateTime.class)
            throw new RuntimeException("@TDateTimeFormat 只能用于 LocalDateTime 类型的字段: " + o2.getClass() + "." + target.getName());

        if (target.getType() != String.class)
            throw new RuntimeException("@TDateTimeFormat 只能用于 String 类型的字段: " + o2.getClass() + "." + target.getName());

        try {
            TDateTimeFormat tDateTimeFormat = target.getAnnotation(TDateTimeFormat.class);
            if (source.get(o1) != null) {
                LocalDateTime value = (LocalDateTime) source.get(o1);
                target.set(o2, value.format(DateTimeFormatter.ofPattern(tDateTimeFormat.pattern())));
            }
        }
        catch (IllegalAccessException e) { throw new RuntimeException(e); }
    }

    private void cAutoUpdate(Field target, Object o2) {
        if (target.getType() != LocalDateTime.class)
            throw new RuntimeException("@CAutoUpdate 只能用于 LocalDateTime 类型的字段: " + o2.getClass() + "." + target.getName());

        try {
            target.set(o2, LocalDateTime.now());
        } catch (IllegalAccessException e) { throw new RuntimeException(e); }
    }
}
